Цель проекта — провести оценку результатов A/B-теста.
Задачи: 1) Оценить корректность проведения теста, в т.ч.:
2) Проанализировать результаты теста.
Данные: основной и вспомогательные датасеты с действиями и характеристиками пользователей.
Техническое задание:
- конверсии в просмотр карточек товаров — событие product_page
- просмотры корзины — product_cart
- покупки — purchase.
Этапы исследования:
# Импортируем нужные библиотеки
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib as plt
import matplotlib.pyplot as plt
import plotly.graph_objs as go
import plotly.express as px
from scipy import stats as st
import math as mth
from datetime import datetime, timedelta
#Откроем файл с данными о маркетинговых событиях
try:
marketing_event = pd.read_csv('/datasets/ab_project_marketing_events.csv')
except:
marketing_event = pd.read_csv('ab_project_marketing_events.csv')
#Откроем файл с данными о новых пользователях
try:
new_users = pd.read_csv('/datasets/final_ab_new_users.csv')
except:
new_users = pd.read_csv('final_ab_new_users.csv')
#Откроем файл с данными о событиях
try:
events = pd.read_csv('/datasets/final_ab_events.csv')
except:
events = pd.read_csv('final_ab_events.csv')
#Откроем файл с данными об участниках теста
try:
participants = pd.read_csv('/datasets/final_ab_participants.csv')
except:
participants = pd.read_csv('final_ab_participants.csv')
# Функция первичного обзора данных
def review(data):
display(i.head(5))
print(i.info())
print('Пропуски:', data.isna().sum())
print('Явные дубликаты:')
if data.duplicated().sum() > 0:
print(data.duplicated().sum())
else:
print('Не найдено')
datalist = [marketing_event, new_users, events, participants]
for i in datalist:
review(i)
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null object 3 finish_dt 14 non-null object dtypes: object(4) memory usage: 576.0+ bytes None Пропуски: name 0 regions 0 start_dt 0 finish_dt 0 dtype: int64 Явные дубликаты: Не найдено
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null object 2 region 61733 non-null object 3 device 61733 non-null object dtypes: object(4) memory usage: 1.9+ MB None Пропуски: user_id 0 first_date 0 region 0 device 0 dtype: int64 Явные дубликаты: Не найдено
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null object 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: float64(1), object(3) memory usage: 13.4+ MB None Пропуски: user_id 0 event_dt 0 event_name 0 details 377577 dtype: int64 Явные дубликаты: Не найдено
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB None Пропуски: user_id 0 group 0 ab_test 0 dtype: int64 Явные дубликаты: Не найдено
Обзор данных:
• /datasets/ab_project_marketing_events.csv — календарь маркетинговых событий на 2020 год;
Структура файла:
• /datasets/final_ab_new_users.csv — все пользователи, зарегистрировавшиеся в интернет-магазине в период с 7 по 21 декабря 2020 года;
Структура файла:
• /datasets/final_ab_events.csv — все события новых пользователей в период с 7 декабря 2020 по 4 января 2021 года;
Структура файла:
• /datasets/final_ab_participants.csv — таблица участников тестов.
Структура файла:
Пропуски есть только в столбце details таблицы с событиями events. В дополнительных данных вполне могут быть такие пропуски, поэтому оставим их как есть.
Типы данных соответствуют содержанию, кроме формата даты. Его нужно заменить на datetime.
Явных дубликатов не выявлено, поверим наличие неявных дубликатов во всех столбцах с типом данных object (не считая идентификатор пользователя user_id и даты).
# Приведем тип данных столбцов с датой к типу datetime
events['event_dt'] = events['event_dt'].astype('datetime64[s]')
marketing_event['start_dt'] = marketing_event['start_dt'].astype('datetime64[s]')
marketing_event['finish_dt'] = marketing_event['finish_dt'].astype('datetime64[s]')
new_users['first_date'] = new_users['first_date'].astype('datetime64[s]')
#Проверка неявных дубликатов
print(sorted(events['event_name'].unique()))
print(sorted(marketing_event['name'].unique()))
print(sorted(marketing_event['regions'].unique()))
print(sorted(new_users['region'].unique()))
print(sorted(new_users['device'].unique()))
print(sorted(participants['group'].unique()))
print(sorted(participants['ab_test'].unique()))
['login', 'product_cart', 'product_page', 'purchase'] ['4th of July Promo', 'Black Friday Ads Campaign', 'CIS New Year Gift Lottery', 'Chinese Moon Festival', 'Chinese New Year Promo', 'Christmas&New Year Promo', 'Dragon Boat Festival Giveaway', 'Easter Promo', "International Women's Day Promo", 'Labor day (May 1st) Ads Campaign', "Single's Day Gift Promo", "St. Patric's Day Promo", "St. Valentine's Day Giveaway", 'Victory Day CIS (May 9th) Event'] ['APAC', 'CIS', 'EU, CIS, APAC', 'EU, CIS, APAC, N.America', 'EU, N.America', 'N.America'] ['APAC', 'CIS', 'EU', 'N.America'] ['Android', 'Mac', 'PC', 'iPhone'] ['A', 'B'] ['interface_eu_test', 'recommender_system_test']
Неявных дубликатов не выявлено. Однако в, в наименованиях A/B-тестов в таблице с участниками participants есть конкурирующий тест - interface_eu_test. Эти данные нужно оставить, т.к. могут быть пользователи, которые попали в оба теста. Наличие таких пересечений проверим при оценке корректности.
# Объединим датасеты с новыми пользователми и участниками теста
participants_total = participants.merge(new_users, on='user_id', how='left')
# Объединим датасеты с актуальными пользователми и событиями, которые они совершали
participants_total = participants_total.merge(events, on='user_id', how='left')
participants_total.info()
participants_total['user_id'].nunique()
<class 'pandas.core.frame.DataFrame'> Int64Index: 110368 entries, 0 to 110367 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 110368 non-null object 1 group 110368 non-null object 2 ab_test 110368 non-null object 3 first_date 110368 non-null datetime64[ns] 4 region 110368 non-null object 5 device 110368 non-null object 6 event_dt 106625 non-null datetime64[ns] 7 event_name 106625 non-null object 8 details 15416 non-null float64 dtypes: datetime64[ns](2), float64(1), object(6) memory usage: 8.4+ MB
16666
# Сохраним в отдельной переменной данные теста recommender_system_test
participants_rst = participants_total.query('ab_test == "recommender_system_test"')
participants_rst.info()
participants_rst['user_id'].nunique()
<class 'pandas.core.frame.DataFrame'> Int64Index: 27724 entries, 0 to 27723 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 27724 non-null object 1 group 27724 non-null object 2 ab_test 27724 non-null object 3 first_date 27724 non-null datetime64[ns] 4 region 27724 non-null object 5 device 27724 non-null object 6 event_dt 24698 non-null datetime64[ns] 7 event_name 24698 non-null object 8 details 3331 non-null float64 dtypes: datetime64[ns](2), float64(1), object(6) memory usage: 2.1+ MB
6701
Всего в объединенном датасете 16666 уникальных пользователей и 110368 событий. Из них 6701 человека являются участниками теста recommender_system_test.
Нам нужно проверить не пересекалось ли по времени проведение теста с различными маркетинговыми событиями.
#Попробуем обрезать дату в событиях
marketing_event.query('start_dt < "2021-01-04" and finish_dt > "2020-12-07"')
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 10 | CIS New Year Gift Lottery | CIS | 2020-12-30 | 2021-01-07 |
Во время указанного времени (финиш позже 12 июля 2020 г., старт раньше 4 января 2021 г.) были проведены две маркетинговые кампании:
Старт этих маркетинговых кампаний совпадает c тестом - набор новых пользователей по условию ТЗ заканчивается 21 декабря 2020 г. Маркетинговые активности могли повлиять на поведение пользователей, однако они касаются всех участников теста. Будем иметь это в виду.
Согласно ТЗ есть следующие ограничения по датам:
Ожидаемый эффект планируется анализировать в течение 14 дней, значит лафтайм пользователей должен быть не более 14 дней.
#Проверим корректность дат для участников теста recommender_system_test.
print('Самая ранняя дата событий', participants_rst['event_dt'].min())
print('Самая поздняя дата в событий', participants_rst['event_dt'].max())
print('Самая ранняя дата регистрации новых пользователей', participants_rst['first_date'].min())
print('Самая поздняя дата регистрации новых пользователей', participants_rst['first_date'].max())
Самая ранняя дата событий 2020-12-07 00:05:57 Самая поздняя дата в событий 2020-12-30 12:42:57 Самая ранняя дата регистрации новых пользователей 2020-12-07 00:00:00 Самая поздняя дата регистрации новых пользователей 2020-12-21 00:00:00
В техническом задании было указано, что датасет final_ab_events.csv содержит записи до 4 января 2021 г., хотя последняя запись имеет дату 30 декабря 2020 г. Тест был остановлен раньше.
Даты регистрации новых пользователей являются корректными - от 7 до 21 декабря 2020 г.
Нам следует отфильтровать пользователей с лайфтаймом больше, чем горизонт событий.
# Рассчитаем лайфтайм для каждого пользователя
lifetime_data = participants_rst.groupby('user_id').agg(
{'first_date':'min', 'event_dt':'max'}).reset_index()
lifetime_data['lifetime'] = (lifetime_data['event_dt']- lifetime_data['first_date']).dt.days
lifetime_data.sort_values(by='lifetime', ascending=False)
| user_id | first_date | event_dt | lifetime | |
|---|---|---|---|---|
| 3013 | 75845C83258FBF73 | 2020-12-07 | 2020-12-30 06:42:52 | 23.0 |
| 5809 | DD4352CDCF8C3D57 | 2020-12-07 | 2020-12-30 12:42:57 | 23.0 |
| 4636 | B0244412983000C5 | 2020-12-07 | 2020-12-29 17:12:44 | 22.0 |
| 2417 | 5E0FE312B9349ADB | 2020-12-07 | 2020-12-29 08:10:51 | 22.0 |
| 4920 | BAAEE6D68FB90D22 | 2020-12-07 | 2020-12-29 10:14:15 | 22.0 |
| ... | ... | ... | ... | ... |
| 6695 | FFB3F647898BA928 | 2020-12-13 | NaT | NaN |
| 6696 | FFC2C5F898D1245B | 2020-12-10 | NaT | NaN |
| 6697 | FFC53FD45DDA5EE8 | 2020-12-19 | NaT | NaN |
| 6698 | FFE858A7845F005E | 2020-12-08 | NaT | NaN |
| 6699 | FFED90241D04503F | 2020-12-08 | NaT | NaN |
6701 rows × 4 columns
# Посчитаем количество пользователей, у который лайфтайм больше 14 дней, и их долю от всех пользователей-участников теста
print('Количество пользователей с лайфтаймом > 14 дн.:',
lifetime_data.query('lifetime>14')['user_id'].count())
print('Процент от общего числа:',
round(lifetime_data.query('lifetime>14')['user_id'].count()/lifetime_data['user_id'].count()*100, 2))
Количество пользователей с лайфтаймом > 14 дн.: 270 Процент от общего числа: 4.03
В датасете участников нашего теста есть 270 человек, у которых лайфтайм больше 14 дней. Они составляют всего 4% от общего числа. Процент незначительный, поэтому можем их удалить.
# Создаем список пользователей с лайфтаймом > 14 дн., подлежащих удалению
lifetime_data_delete = lifetime_data.query('lifetime>14')['user_id']
participants_rst = participants_rst.query('user_id not in @lifetime_data_delete')
participants_rst.info()
participants_rst['user_id'].nunique()
<class 'pandas.core.frame.DataFrame'> Int64Index: 25492 entries, 8 to 27723 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 25492 non-null object 1 group 25492 non-null object 2 ab_test 25492 non-null object 3 first_date 25492 non-null datetime64[ns] 4 region 25492 non-null object 5 device 25492 non-null object 6 event_dt 22466 non-null datetime64[ns] 7 event_name 22466 non-null object 8 details 3071 non-null float64 dtypes: datetime64[ns](2), float64(1), object(6) memory usage: 1.9+ MB
6431
После удаления пользователей с лайфтаймом больше 14 дней, в датасете участников теста осталось 6431 человек.
Проверим соответствие участников теста условиям ТЗ:
Дополнительно изучим:
#Посчитаем количество участников
print('Фактическое количество участников теста:', participants_rst['user_id'].nunique())
#Доля несоответствия фактического количества участников теста ожидаемому количеству в 6000 человек
print('Процент несоответствия ожидаемому количеству участников теста:',
round((participants_rst['user_id'].nunique()-6000)/6000*100,2))
Фактическое количество участников теста: 6431 Процент несоответствия ожидаемому количеству участников теста: 7.18
#Посчитаем число уникальных пользователей в каждой из групп теста recommender_system_test
groups = participants_rst.groupby('group').agg({'user_id': 'nunique'}).reset_index()
groups.rename(columns = {'user_id':'users'}, inplace = True)
groups['share, %'] = round((groups['users']/groups['users'].sum()*100),2)
groups
| group | users | share, % | |
|---|---|---|---|
| 0 | A | 3668 | 57.04 |
| 1 | B | 2763 | 42.96 |
Участников из тестовой группы A (3668 чел.) больше, чем из контрольной группы B (2763 чел.). Размеры групп различаются на 905 человек. Всего участников теста до возможной чистки - 6431 человек, что больше ожидаемого по ТЗ на 7.18%.
Проверим есть ли пользователи, которые находятся в обеих тестовых группах.
#Проверим есть ли пользователи, которые находятся в обоих тестовых группах recommender_system_test
duplicated_users = participants_rst.groupby('user_id').agg(
{'group': 'nunique'}).sort_values(by = 'group', ascending = False).reset_index()
duplicated_users = duplicated_users[duplicated_users['group']==2]
print('Количество дублирующихся участников:', duplicated_users['user_id'].count())
print('Процент дублирующихся участников:',\
round(duplicated_users['user_id'].count()/participants_rst['user_id'].nunique()*100, 2))
Количество дублирующихся участников: 0 Процент дублирующихся участников: 0.0
Дублирующихся в обоих группах участников теста recommender_system_test не найдено. Далее проверим нет ли пересечений между нашим тестом recommender_system_test и конкурирующим тестом interface_eu_test.
#Посчитаем число уникальных пользователей в каждой из групп тестов recommender_system_test и interface_eu_test
test_groups = participants_total.groupby(['ab_test','group']).agg({'user_id': 'nunique'}).reset_index()
test_groups.rename(columns = {'user_id':'users'}, inplace = True)
test_groups
| ab_test | group | users | |
|---|---|---|---|
| 0 | interface_eu_test | A | 5831 |
| 1 | interface_eu_test | B | 5736 |
| 2 | recommender_system_test | A | 3824 |
| 3 | recommender_system_test | B | 2877 |
#Проверим есть ли пользователи, которые находятся в обоих тестах recommender_system_test и interface_eu_test
duplicated_test_users = participants_total.groupby('user_id').agg(
{'ab_test': 'nunique'}).sort_values(by = 'ab_test', ascending = False).reset_index()
duplicated_test_users = duplicated_test_users[duplicated_test_users['ab_test']==2]['user_id']
print('Количество дублирующихся участников:', duplicated_test_users.count())
print('Доля дублирующихся участников:',\
round(duplicated_test_users.count()/participants_total['user_id'].nunique()*100, 2))
Количество дублирующихся участников: 1602 Доля дублирующихся участников: 9.61
Тесты были проведены в одно время, поэтому получилось так, что их участники пересекаются. В датасете есть 1602 пользователя (9.61% от общего числа), которые стали участниками обоих тестов recommender_system_test и interface_eu_test. Участие в двух тестах может исказить результаты, поэтому удалим их данные из даатсета.
# Создаем список пользователей с лайфтаймом > 14 дн., подлежащих удалению
participants_rst = participants_rst.query('user_id not in @duplicated_test_users')
participants_rst.info()
participants_rst['user_id'].nunique()
<class 'pandas.core.frame.DataFrame'> Int64Index: 19443 entries, 8 to 27723 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 19443 non-null object 1 group 19443 non-null object 2 ab_test 19443 non-null object 3 first_date 19443 non-null datetime64[ns] 4 region 19443 non-null object 5 device 19443 non-null object 6 event_dt 17132 non-null datetime64[ns] 7 event_name 17132 non-null object 8 details 2291 non-null float64 dtypes: datetime64[ns](2), float64(1), object(6) memory usage: 1.5+ MB
4893
После удаления пересекающихся с другим тестом пользователей, количество участников теста recommender_system_test уменьшилось до 4893 человек.
В датасете participants_rst есть пустые значения в столбцах с датой и наименованием событий - event_dt и event_name. Оценим количество уникальных пользователей, у которых нет совершенных событий.
# Выведем количество участников теста без событий
user_events = participants_rst.groupby(['user_id', 'group']).agg(
{'event_name': 'count'}).reset_index().sort_values(by='event_name', ascending=False)
user_no_events = user_events.groupby('user_id').agg(
{'event_name': 'sum'}).reset_index().sort_values(by='event_name', ascending=False)
user_no_events = user_no_events.loc[user_no_events['event_name']==0]
print('Участники теста без событий:', user_no_events['user_id'].nunique())
print('Доля неактивных участников:',\
round(user_no_events['user_id'].nunique()/participants_rst['user_id'].nunique()*100, 2))
Участники теста без событий: 2311 Доля неактивных участников: 47.23
2311 участников (47.23%) теста не совершили за период теста ни одного события. Число совпадает с количеством пропусков в датасете.
Попытаемся выяснить с чем это связано.
# Проверим даты регистрации участников без событий
user_no_events = participants_rst.loc[participants_rst['event_name'].isna()]
print(user_no_events['first_date'].min())
print(user_no_events['first_date'].max())
2020-12-07 00:00:00 2020-12-21 00:00:00
Участники без событий регистрировались в приложении с 7 по 21 декабря, то есть на протяжении всего тестового периода. Проверим есть ли особенности в этом, возможно в определенные даты в приложении были технические сложности.
# Визуализируем распределение участников без событий по времени регистрации и сравним со всеми участниками
user_no_events['first_date'].hist(bins=14, figsize=(12,6), label='user_no_events', alpha=0.5)
participants_rst['first_date'].hist(bins=14, figsize=(12,6), label='all_users', alpha=0.5)
plt.legend()
plt.xlabel('Распределение участников по времени регистрации')
plt.ylabel('Количество наблюдений')
plt.title('Распределение пользователей по частоте событий')
Text(0.5, 1.0, 'Распределение пользователей по частоте событий')
Участники без событий фиксируются на протяжении всего тестового периода. Больше всего их было зарегистрировано 13 декабря 2020 г. С 14 декабря 2020 г. регистраций новых пользователей стало больше, но количество неактивных пользователей не выросло.
# Проверим какие устройства использовали участники без событий
user_no_events['device'].value_counts()
Android 1046 PC 572 iPhone 493 Mac 200 Name: device, dtype: int64
# Проверим какие устройства обычно используют все участники при совершении событий
participants_rst['device'].value_counts()
Android 8600 PC 4770 iPhone 4182 Mac 1891 Name: device, dtype: int64
При анализе устройств, которые использовали участники без событий, также не выявлено особенностей.
# Проверим какие регионы представляют участники без событий
user_no_events['region'].value_counts()
EU 2155 N.America 104 APAC 27 CIS 25 Name: region, dtype: int64
# Проверим какие регионы представляют все участники при совершении событий
participants_rst['region'].value_counts()
EU 18143 N.America 867 APAC 233 CIS 200 Name: region, dtype: int64
При анализе регионов участников без событий также не выявлено особенностей.
# Проверим как участники без событий распределены между группами
user_no_events_id = user_no_events['user_id']
user_no_events_groups = participants_rst.query('user_id in @user_no_events_id')
user_no_events_groups = user_no_events_groups.groupby('group').agg({'user_id': 'nunique'}).reset_index()
user_no_events_groups
| group | user_id | |
|---|---|---|
| 0 | A | 821 |
| 1 | B | 1490 |
# Проверим как распределены между группами все участники при совершении событий
groups = participants_rst.groupby('group').agg({'user_id': 'nunique'}).reset_index()
groups = groups.merge(user_no_events_groups, on = 'group')
groups.columns = ['group', 'users', 'users_no_events']
groups['share, %'] = round(groups['users_no_events']/groups['users']*100,2)
groups
| group | users | users_no_events | share, % | |
|---|---|---|---|---|
| 0 | A | 2783 | 821 | 29.50 |
| 1 | B | 2110 | 1490 | 70.62 |
В контрольной группе значительно выше доля пользователей (70.62%), не совершавших события. В тестовой группе неактивных пользователей всего 29.5%.
В целом, при наличии таких пользователей группы становятся неоднородными. Поэтому удалим неактивных пользователей из датасета.
# Создаем список пользователей с лайфтаймом > 14 дн., подлежащих удалению
participants_rst = participants_rst.query('user_id not in @user_no_events_id')
participants_rst.info()
participants_rst['user_id'].nunique()
<class 'pandas.core.frame.DataFrame'> Int64Index: 17132 entries, 8 to 27723 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 17132 non-null object 1 group 17132 non-null object 2 ab_test 17132 non-null object 3 first_date 17132 non-null datetime64[ns] 4 region 17132 non-null object 5 device 17132 non-null object 6 event_dt 17132 non-null datetime64[ns] 7 event_name 17132 non-null object 8 details 2291 non-null float64 dtypes: datetime64[ns](2), float64(1), object(6) memory usage: 1.3+ MB
2582
После удаления неактивных пользователей, количество участников теста recommender_system_test уменьшилось до 2582 человека.
Ожидаемое количество новых пользователей из ЕС было указано как 15%. Проверим сколько пользователей пришло в разбивке по регионам.
# Разобъем участников теста и всех пользователей по странам
region_participants = participants_rst.pivot_table(index=['region'], values=['user_id'], aggfunc='nunique').sort_values(
by='user_id', ascending=False).reset_index()
region_users = participants_total.groupby('region').agg({'user_id': 'nunique'}).reset_index().sort_values(by='user_id', ascending=False)
regions = region_users.merge(region_participants, on = 'region')
regions.columns = ['region', 'users', 'participants']
# Выведем процент
regions['share, %'] = round(regions['participants']/regions['users']*100,2)
regions
| region | users | participants | share, % | |
|---|---|---|---|---|
| 0 | EU | 16316 | 2403 | 14.73 |
| 1 | N.America | 223 | 110 | 49.33 |
| 2 | APAC | 72 | 42 | 58.33 |
| 3 | CIS | 55 | 27 | 49.09 |
Процент участников теста из ЕС от всех новых пользователей из ЕС составяет 14.73%, что примерно соответсвует заявленным в ТЗ 15%.
Проверим заново количество участников теста и как они распределены по группам после всех манипуляций.
#Посчитаем количество участников
print('Фактическое количество участников теста:', participants_rst['user_id'].nunique())
#Доля несоответствия фактического количества участников теста ожидаемому количеству в 6000 человек
print('Процент несоответствия ожидаемому количеству участников теста:',
round((participants_rst['user_id'].nunique()-6000)/6000*100,2))
Фактическое количество участников теста: 2582 Процент несоответствия ожидаемому количеству участников теста: -56.97
#Посчитаем число уникальных пользователей в каждой из групп теста recommender_system_test
groups = participants_rst.groupby('group').agg({'user_id': 'nunique'}).reset_index()
groups.rename(columns = {'user_id':'users'}, inplace = True)
groups['share, %'] = round((groups['users']/groups['users'].sum()*100),2)
groups
| group | users | share, % | |
|---|---|---|---|
| 0 | A | 1962 | 75.99 |
| 1 | B | 620 | 24.01 |
1) В техническом задании было указано, что датасет final_ab_events.csv содержит записи до 4 января 2021 г., хотя последняя запись имеет дату 30 декабря 2020 г. Тест был остановлен раньше.
Даты регистрации новых пользователей являются корректными - от 7 до 21 декабря 2020 г.
2) Во время проведения теста были проведены две маркетинговые кампании:
Маркетинговые активности могли повлиять на поведение пользователей, однако они касаются всех групп теста одинаково. Будем иметь это в виду.
3) Дублирующихся в обоих группах участников теста recommender_system_test не найдено.
4) Есть 1602 пользователя (9.61% от общего числа), которые стали участниками обоих тестов recommender_system_test и interface_eu_test. Тесты были проведены в одно время, поэтому получилось так, что их некоторые их участники пересекаются. Участие в двух тестах может исказить результаты, поэтому их данные из датасета удалены.
5) 2311 участников (47.23%) теста не совершили за период теста ни одного события. Эти случаи не связаны с датами, девайсами. В контрольной группе значительно выше доля пользователей (70.62%), не совершавших события. В тестовой группе эта доля составляет всего 29.5%. В целом, при наличии таких пользователей группы становятся неоднородными. Поэтому неактивных пользователей из датасета удалили.
6) Процент участников теста из ЕС от всех новых пользователей из ЕС составяет 14.73%, что примерно соответсвует заявленным в ТЗ 15%.
7) До чистки участников теста было 6431 чел. (больше ожидаемого на 7.18%). Участников из тестовой группы A (3668 чел.) было больше, чем из контрольной группы B (2763 чел.). Размеры групп различаются на 905 человек.
После чистки участников теста стало 2582 чел. (меньше ожидаемого на 56.97%). Участников из тестовой группы A (1962 чел.) было больше, чем из контрольной группы B (620 чел.). Размеры групп различаются на 1342 человек.
Изучим как распределено количество событий на пользователя в разбивке на группы.
events_users = participants_rst.groupby(['group', 'user_id'], as_index=False).agg({'event_name':'count'})
events_users_groups = events_users.groupby('group').agg({'event_name':'mean'})
events_users_groups
| event_name | |
|---|---|
| group | |
| A | 7.005607 |
| B | 5.462903 |
В среднем частота событий на одного пользователя в группе A составляет 7 единиц, а в группе B - 5 единиц.
# Разобъем таблицу с частотой событий events_users по группам
events_users_a = events_users[events_users['group']=='A']
events_users_b = events_users[events_users['group']=='B']
# Визуализируем распределение частоты событий по группам
events_users_a['event_name'].hist(bins=25, figsize=(12,6), label='A', alpha=0.5)
events_users_b['event_name'].hist(bins=25, figsize=(12,6), label='B', alpha=0.5)
plt.legend()
plt.xlabel('Частота событий на пользователя')
plt.ylabel('Количество пользователей')
plt.title('Распределение пользователей по частоте событий')
Text(0.5, 1.0, 'Распределение пользователей по частоте событий')
По гистограмме видно, что оба распределения имеют схожие формы, просто частота событий в тестовой группе A больше.
# Разбиваем таблицу по группам
participants_rst_a = participants_rst[participants_rst['group']=='A']
participants_rst_b = participants_rst[participants_rst['group']=='B']
# Визуализируем распределение регистрации новых пользователей по дням
participants_rst_a['first_date'].hist(bins=14, figsize=(12,6), label='A', alpha=0.5)
participants_rst_b['first_date'].hist(bins=14, figsize=(12,6), label='B', alpha=0.5)
plt.legend()
plt.xlabel('Дата регистрации')
plt.ylabel('Количество пользователей')
plt.title('Распределение пользователей по дате регистрации')
Text(0.5, 1.0, 'Распределение пользователей по дате регистрации')
У тестовой группы A набор новых пользователей активизировался с 14 декабря 2020 г., а у контрольно группы B все осталось в прежнем темпе. До этой даты темпы регистрации новых пользователей были примерно одинаковыми. Сложно предположить с чем э это связано, но это точно нехорошо будет влиять на однородность групп.
# Добавим столбец со значением даты события (без времени)
participants_rst['date'] = participants_rst['event_dt'].dt.date
participants_rst.head()
| user_id | group | ab_test | first_date | region | device | event_dt | event_name | details | date | |
|---|---|---|---|---|---|---|---|---|---|---|
| 8 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone | 2020-12-20 15:46:06 | product_page | NaN | 2020-12-20 |
| 9 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone | 2020-12-21 00:40:59 | product_page | NaN | 2020-12-21 |
| 10 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone | 2020-12-25 05:19:45 | product_page | NaN | 2020-12-25 |
| 11 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone | 2020-12-20 15:46:02 | login | NaN | 2020-12-20 |
| 12 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone | 2020-12-21 00:40:58 | login | NaN | 2020-12-21 |
participants_rst_a = participants_rst[participants_rst['group']=='A']
participants_rst_b = participants_rst[participants_rst['group']=='B']
# Визуализируем события по дням
fig = plt.figure(figsize=(15,15))
ax = participants_rst_a.pivot_table(
index='date', values='user_id', columns='event_name', aggfunc='count').plot.bar(
stacked=True, ylim=(0, 2200))
ax.set_title('События группы A по дням')
ax = participants_rst_b.pivot_table(
index='date', values='user_id', columns='event_name', aggfunc='count').plot.bar(
stacked=True, ylim=(0, 500))
ax.set_title('События группы B по дням')
Text(0.5, 1.0, 'События группы B по дням')
<Figure size 1080x1080 with 0 Axes>
Поведение пользователей разных групп заметно различается. Активность группы A резко возросла с 14 декабря 2020 г. в связи с ростом числа новых пользователей, а у группы B изначально была хорошая активность. В обеих группах пик активности был 21 декабря 2020 г., потом все плавно пошло на спад до конца периода. Снижение активности можно объяснить приближением праздничных дней.
Ранее в п. 3.1. мы видели, что в конце декабря было проведено 2 маркетинговые акции:
По графику можно сказать, что заметного влияния не поведение пользователей эти акции не оказали.
В датасете фиксируется 4 вида события:
# Постчитаем количество уникальных пользователей по видам событий и конверсию
events_count = participants_rst.groupby('event_name').agg(
{'user_id':'nunique'}).sort_values(by='user_id', ascending=False).reset_index()
events_count
| event_name | user_id | |
|---|---|---|
| 0 | login | 2582 |
| 1 | product_page | 1629 |
| 2 | purchase | 798 |
| 3 | product_cart | 780 |
По количеству уникальных пользователей до оплаты дошло чуть меньше людей, чем до открытия корзины, что нарушает логику воронки. Возможно некоторые пользователи оплачивают товары пользуясь функцией "быстрой оплаты" / "мгновенной покупки". Поэтому добавим столбец с нумерацией этапов воронки, чтобы не нарушать поэтапность из-за количества пользователей.
# Добавим столбец с нумерацией этапов воронки и отсортируем по этапам
events_count['funnel_number'] = [1, 2, 4, 3]
events_count = events_count.sort_values(by='funnel_number', ascending=True)
# Считаем конверсию
events_count['initial, %'] = round((events_count['user_id'] * 100/ participants_rst['user_id'].nunique()),2)
events_count['conversion, %'] = round((100+events_count['user_id'].pct_change() * 100),2)
events_count = events_count.fillna(100)
events_count
| event_name | user_id | funnel_number | initial, % | conversion, % | |
|---|---|---|---|---|---|
| 0 | login | 2582 | 1 | 100.00 | 100.00 |
| 1 | product_page | 1629 | 2 | 63.09 | 63.09 |
| 3 | product_cart | 780 | 3 | 30.21 | 47.88 |
| 2 | purchase | 798 | 4 | 30.91 | 102.31 |
# Визуализируем воронку
fig = px.funnel(events_count, y='event_name', x='user_id', title="Воронка событий")
fig.show()
Больше всего пользователей теряется между регистрацией login и просмотром продукта product_page.
В целом до целевого события (purchase) доходит 798 чел. (30.91% пользователей), а до корзины product_cart 780 человек (30.21% пользователей).
Особенности данных нужно учесть, прежде чем приступать к A/B-тестированию:
В среднем частота событий на одного пользователя в группе A составляет 7 единиц, а в группе B 5 единиц. Активность тестовой группы выше в 2-3 раза.
У тестовой группы A набор новых пользователей сильно активизировался с 14 декабря 2020 г., а у контрольно группы B все осталось в прежнем темпе. До этой даты темпы регистрации новых пользователей были примерно одинаковыми. Сложно предположить с чем это связано, но это точно нехорошо будет влиять на однородность групп.
Поведение пользователей разных групп заметно различается во времени. Активность группы A резко возросла с 14 декабря 2020 г., а у группы B изначально была хорошая активность. В обеих группах пик активности был 21 декабря 2020 г., потом плавно пошло на спад до конца периода. Снижение активности обеих групп можно объяснить приближением праздничных дней.
Праздничные дни могли сильно исказить результаты, желательно было бы провести тест в более спокойное и стабильное время.
Маркетинговые акции Christmas&New Year Promo и CIS New Year Gift Lottery заметного влияния не поведение пользователей не оказали.
По количеству уникальных пользователей до оплаты дошло чуть меньше людей, чем до открытия корзины, что нарушает логику воронки. Возможно некоторые пользователи оплачивают товары пользуясь функцией "быстрой оплаты" / "мгновенной покупки"
Больше всего пользователей теряется между регистрацией login и просмотром продукта product_page. В целом до целевого события (purchase) доходит 798 чел. (30.91% пользователей), а до корзины product_cart 780 человек (30.21% пользователей).
# Подготовим данные для расчета конверсии в разбивке на группы
test_data_a = participants_rst_a.groupby(['group', 'event_name'])['user_id'].nunique().reset_index()
test_data_b = participants_rst_b.groupby(['group', 'event_name'])['user_id'].nunique().reset_index()
# Считаем конверсию для обеих групп
test_data = [test_data_a, test_data_b]
for i in test_data:
i = i.sort_values(by='user_id', ascending=False)
i['funnel_number'] = [1, 2, 4, 3]
i = i.sort_values(by='funnel_number', ascending=True)
i['initial, %'] = round((i['user_id'] * 100/ i['user_id'].sum()),1)
i['conversion, %'] = round((100+i['user_id'].pct_change() * 100),2)
i = i.fillna(100)
display(i)
fig = px.funnel(i, y='event_name', x='user_id', title="Воронка событий")
fig.show()
| group | event_name | user_id | funnel_number | initial, % | conversion, % | |
|---|---|---|---|---|---|---|
| 0 | A | login | 1962 | 1 | 43.9 | 100.00 |
| 2 | A | product_page | 1280 | 2 | 28.6 | 65.24 |
| 1 | A | product_cart | 605 | 3 | 13.5 | 47.27 |
| 3 | A | purchase | 622 | 4 | 13.9 | 102.81 |
| group | event_name | user_id | funnel_number | initial, % | conversion, % | |
|---|---|---|---|---|---|---|
| 0 | B | login | 620 | 1 | 47.0 | 100.00 |
| 2 | B | product_page | 349 | 2 | 26.4 | 56.29 |
| 1 | B | product_cart | 175 | 3 | 13.3 | 50.14 |
| 3 | B | purchase | 176 | 4 | 13.3 | 100.57 |
По таблицам и визуализации можно сказать, что ощутимое улучшение есть только на этапе просмотра карточке товаров - product_page.
Ожидаемый эффект, что за 14 дней с момента регистрации в системе пользователи покажут улучшение конверсии не менее, чем на 10%, оправдать не удалось.
# Подготовим данные для проведения теста
test_data_pivot = participants_rst.pivot_table(
index='event_name',columns='group', values='user_id', aggfunc='nunique').reset_index()
test_data_pivot = test_data_pivot.sort_values(by='A', ascending=False)
test_data_pivot['funnel_number'] = [1, 2, 4, 3]
test_data_pivot = test_data_pivot.sort_values(by='funnel_number', ascending=True)
test_data_pivot
| group | event_name | A | B | funnel_number |
|---|---|---|---|---|
| 0 | login | 1962 | 620 | 1 |
| 2 | product_page | 1280 | 349 | 2 |
| 1 | product_cart | 605 | 175 | 3 |
| 3 | purchase | 622 | 176 | 4 |
Для проведения теста будем использовать - гипотезы о равенстве долей. Сформулируем нулевую и альтернативную гипотезу:
Нулевая гипотеза: Конверсии между этапами воронки в обеих группах не имеют статистических различий.
Альтернативная гипотеза: Конверсии между этапами воронки в обеих группах разные.
Подготовка к тестированию:
У нас есть три события (не считая login), и в ходе тестирования нужно будет сравнивать конверсии на всех трех этапах. Значит нам предстоит провести три A/B-тестирования.
Это множественные сравнения, поэтому нужно применить поправку Бонферрони - статистическую значимость нужно "разбить" на количество сравнений. Их у нас в сумме 3 единицы, поэтому bonferroni_alpha = alpha / 3.
# Задаем функцию проверки равенства долей
def z_test(successes, trials, alpha):
alpha = alpha
bonferroni_alpha = alpha / 3
successes = successes
trials = trials
# Пропорция успехов в первой группе:
p1 = successes[0]/trials[0]
# Пропорция успехов во второй группе:
p2 = successes[1]/trials[1]
print(successes[0], successes[1],trials[0] ,trials[1])
# Пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
# Разница пропорций в датасетах
difference = p1 - p2
# Считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
# Задаем стандартное нормальное распределение
distr = st.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: ', p_value)
if p_value < bonferroni_alpha:
print('Отвергаем нулевую гипотезу, конверсия в обеих группах различается')
else:
print('Не получилось отвергнуть нулевую гипотезу, конверсия в обеих группах равна')
alpha = .05
trials = [test_data_pivot.iloc[0]['A'], test_data_pivot.iloc[0]['B']]
s = {'product_page':[test_data_pivot.iloc[1]['A'],test_data_pivot.iloc[1]['B']],
'product_cart':[test_data_pivot.iloc[2]['A'],test_data_pivot.iloc[2]['B']],
'purchase':[test_data_pivot.iloc[3]['A'],test_data_pivot.iloc[3]['B']]}
for i in ['product_page', 'product_cart', 'purchase']:
successes = s[i]
print(f'Тестируем конверсию события "{i}"')
z_test(successes, trials, alpha)
print()
Тестируем конверсию события "product_page" 1280 349 1962 620 p-значение: 5.6894128467988025e-05 Отвергаем нулевую гипотезу, конверсия в обеих группах различается Тестируем конверсию события "product_cart" 605 175 1962 620 p-значение: 0.21726998312528423 Не получилось отвергнуть нулевую гипотезу, конверсия в обеих группах равна Тестируем конверсию события "purchase" 622 176 1962 620 p-значение: 0.11942624966751758 Не получилось отвергнуть нулевую гипотезу, конверсия в обеих группах равна
Итог: В результате тестирования, статистически значимая разница конверсий в группах А и В не выявлена на втором и третьем шаге воронки (product_cart и purchase), на первом шаге (product_page) найти статистически значимую разницу удалось. Значит внедрение рекомендательной системы recommender_system_test способствует только улучшению просматриваемости карточек товаров.
Учитывая особенности условий проведения тестирования и недостаточно корректных данных можно сделать вывод, что эффективность внедрения рекомендательной системы recommender_system_test при данных условиях доказать не удалось.
На старте мы имели 2 датасета - один об источниках, с которого пользователи установили приложение, второй о дейтсвиях пользователей внутри приложения. Данные являются полными, не содержат пропусков.
Что изменилось после предобработки:
Оценка корректности данных: 1) Тест был остановлен раньше, последняя запись имеет дату 30 декабря 2020 г. Даты регистрации новых пользователей являются корректными - от 7 до 21 декабря 2020 г.
2) Во время проведения теста были проведены две маркетинговые кампании:
Маркетинговые активности могли повлиять на поведение пользователей, однако они касаются всех групп теста одинаково. Будем иметь это в виду.
3) Дублирующихся в обоих группах участников теста recommender_system_test не найдено.
4) Есть 1602 пользователя (9.61% от общего числа), которые стали участниками обоих тестов recommender_system_test и interface_eu_test. Тесты были проведены в одно время, поэтому получилось так, что их некоторые их участники пересекаются. Участие в двух тестах может исказить результаты, поэтому их данные из датасета удалены.
5) 2311 участников (47.23%) теста не совершили за период теста ни одного события. Эти случаи не связаны с датами, девайсами. В контрольной группе значительно выше доля пользователей (70.62%), не совершавших события. В тестовой группе эта доля составляет всего 29.5%. В целом, при наличии таких пользователей группы становятся неоднородными. Поэтому неактивных пользователей из датасета удалили.
6) Процент участников теста из ЕС от всех новых пользователей из ЕС составяет 14.73%, что примерно соответсвует заявленным в ТЗ 15%.
7) До чистки участников теста было 6431 чел. (больше ожидаемого на 7.18%). Участников из тестовой группы A (3668 чел.) было больше, чем из контрольной группы B (2763 чел.). Размеры групп различаются на 905 человек.
После чистки участников теста стало 2582 чел. (меньше ожидаемого на 56.97%). Участников из тестовой группы A (1962 чел.) было больше, чем из контрольной группы B (620 чел.). Размеры групп различаются на 1342 человек.
Исследовательский анализ данных выявил следующие инсайты:
В среднем частота событий на одного пользователя в группе A составляет 7 единиц, а в группе B 5 единиц. Активность тестовой группы выше в 2-3 раза.
У тестовой группы A набор новых пользователей сильно активизировался с 14 декабря 2020 г., а у контрольно группы B все осталось в прежнем темпе. До этой даты темпы регистрации новых пользователей были примерно одинаковыми. Сложно предположить с чем это связано, но это точно нехорошо будет влиять на однородность групп.
Поведение пользователей разных групп заметно различается во времени. Активность группы A резко возросла с 14 декабря 2020 г., а у группы B изначально была хорошая активность. В обеих группах пик активности был 21 декабря 2020 г., потом плавно пошло на спад до конца периода. Снижение активности обеих групп можно объяснить приближением праздничных дней.
Праздничные дни могли сильно исказить результаты, желательно было бы провести тест в более спокойное и стабильное время.
Маркетинговые акции Christmas&New Year Promo и CIS New Year Gift Lottery заметного влияния не поведение пользователей не оказали.
По количеству уникальных пользователей до оплаты дошло чуть меньше людей, чем до открытия корзины, что нарушает логику воронки. Возможно некоторые пользователи оплачивают товары пользуясь функцией "быстрой оплаты" / "мгновенной покупки"
Больше всего пользователей теряется между регистрацией login и просмотром продукта product_page. В целом до целевого события (purchase) доходит 798 чел. (30.91% пользователей), а до корзины product_cart 780 человек (30.21% пользователей).
Проверка результатов теста:
ИТОГО: Учитывая особенности условий проведения тестирования и недостаточно корректных данных можно сделать вывод, что эффективность внедрения реклмендательной системы recommender_system_test доказать не удалось.
Рекомендации: